package com.nulldev.util.networking.npacket4.extensions.protocol;

import static com.nulldev.util.networking.npacket4.util.nPacketUtil.__readInt;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import com.nulldev.util.VariableAPI.ArrayUtils;
import com.nulldev.util.data.Variables;
import com.nulldev.util.data.Charsets.CharsetUtil;
import com.nulldev.util.data.CompressionAPI.LZString.LZString;
import com.nulldev.util.data.CompressionAPI.thirdparty.airlift.snappy.SnappyCompressor;
import com.nulldev.util.data.CompressionAPI.thirdparty.airlift.snappy.SnappyDecompressor;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.networking.npacket4.extensions.PacketExtension;
import com.nulldev.util.networking.npacket4.extensions.PacketExtensions.PacketExtensionID;

public class PacketCompressionExtension implements PacketExtension {

	private static final long serialVersionUID = 7526380838133077451L;
	private volatile String method;

	/*
	 * TODO: Add HS (Heatshrink) support.
	 */

	private final static String[] accepted_methods = new String[]
		{ "GZIP", "LZSTRING", "SNAPPY" };

	public PacketCompressionExtension(final String method) {
		if (ArrayUtils.StringArrayUtils.containsInStringOrStartsWith_NoCase(accepted_methods, method)) {
			this.method = method;
		} else {
			throw new UnsupportedOperationException("Unsupported compression method: " + method);
		}
	}

	public PacketCompressionExtension() {
	}

	@Override
	public PacketCompressionExtension readExtensionData(final InputStream dis) throws IOException {
		// EXT_ID already read.
		final int method_name_length = __readInt(dis);
		byte[] method = new byte[method_name_length];
		dis.read(method);
		this.method = new String(method);
		return this;
	}

	@Override
	public int extensionID() {
		return PacketExtensionID.EXT_COMPRESSION;
	}

	@Override
	public byte[] extensionData() {
		final ByteArrayOutputStream baos = new ByteArrayOutputStream();
		final DataOutputStream dos = new DataOutputStream(baos);
		try {
			dos.writeInt(this.extensionID());
			dos.flush();
			dos.writeInt(this.method.length());
			dos.flush();
			dos.write(this.method.getBytes());
			dos.flush();
		} catch (IOException e2) {
			throw new RuntimeException("extensionData() -> Failed to write extension data.", e2);
		}
		try {
			dos.flush();
			baos.flush();
		} catch (IOException e1) {
		}
		final byte[] data = baos.toByteArray();
		IOUtils.closeQuietly(baos);
		return data;
	}

	@Override
	public byte[] processData(final byte[] data) {
		if (this.method.equalsIgnoreCase("GZIP")) {
			return __gzip_decompress(data);
		} else if (this.method.equalsIgnoreCase("LZSTRING")) {
			return LZString.decompress(new String(data, CharsetUtil.UTF_8)).getBytes(CharsetUtil.UTF_8);
		} else if (this.method.equalsIgnoreCase("SNAPPY")) {
			return __snappy_decompress(data);
		} else {
			return data;
		}
	}

	public byte[] compress(final byte[] data) {
		if (this.method.equalsIgnoreCase("GZIP")) {
			return __gzip_compress(data);
		} else if (this.method.equalsIgnoreCase("LZSTRING")) {
			return LZString.compress(new String(data, CharsetUtil.UTF_8)).getBytes(CharsetUtil.UTF_8);
		} else if (this.method.equalsIgnoreCase("SNAPPY")) {
			return __snappy_compress(data);
		} else {
			return data;
		}
	}

	private static byte[] __snappy_compress(final byte[] uncompressedData) {
		final SnappyCompressor sc = new SnappyCompressor();
		final byte[] output = new byte[sc.maxCompressedLength(uncompressedData.length)];
		sc.compress(uncompressedData, output);
		return output;
	}

	private static byte[] __snappy_decompress(final byte[] compressedData) {
		final SnappyDecompressor sc = new SnappyDecompressor();
		final byte[] output = new byte[SnappyDecompressor.getUncompressedLength(compressedData, 0)];
		sc.decompress(compressedData, output);
		return output;
	}

	private static byte[] __gzip_compress(final byte[] uncompressedData) {
		ByteArrayOutputStream bos = null;
		GZIPOutputStream gzipOS = null;
		try {
			bos = new ByteArrayOutputStream(uncompressedData.length);
			gzipOS = new GZIPOutputStream(bos);
			gzipOS.write(uncompressedData);
			gzipOS.flush();
			gzipOS.finish();
			gzipOS.close();
			return bos.toByteArray();

		} catch (IOException e) {
			throw new RuntimeException(e);
		} finally {
			try {
				Variables.notNullE(gzipOS);
				gzipOS.close();
				bos.close();
			} catch (Exception ignored) {
			}
		}
	}

	private byte[] __gzip_decompress(final byte[] compressedData) {
		ByteArrayInputStream bis = null;
		ByteArrayOutputStream bos = null;
		GZIPInputStream gzipIS = null;

		try {
			bis = new ByteArrayInputStream(compressedData);
			bos = new ByteArrayOutputStream();
			gzipIS = new GZIPInputStream(bis);

			byte[] buffer = new byte[1024];
			int len;
			while ((len = gzipIS.read(buffer)) != -1) {
				bos.write(buffer, 0, len);
				bos.flush();
			}
			return bos.toByteArray();
		} catch (IOException e) {
			throw new RuntimeException(e);
		} finally {
			try {
				Variables.notNullE(gzipIS);
				gzipIS.close();
				bos.close();
				bis.close();
			} catch (Exception ignored) {
			}
		}
	}

}
